Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 087fd6ea74d4fcb69048b9e77541d6bc9477dcd1


Parents : 514005e
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-24T04:58:09-05:00

refactor: create main-process logging by implementing a durable logger that appends to meshchatx.log

Changes

5 files changed, 220 insertions(+), 9 deletions(-)

M CHANGELOG.md +1 -1

Diff

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bca78d16..9d1884d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -57,7 +57,7 @@ All notable changes to this project will be documented in this file.
### Fixed
-- Desktop AppImage: closed stdout no longer raises a main-process **write EPIPE** dialog when MeshChatX is backgrounded. Logging uses broken-pipe guards
+- Desktop AppImage: main-process logs always append to storage `logs/meshchatx.log`. Stdout is only used when a TTY is attached, with broken-pipe guards as a fallback, so background launches no longer raise **write EPIPE** dialogs
- Android: lxmfy packaging, flock soft-lock, splash/logo clipping, Landlock skipped on Android
- Android RNode BLE/USB via Chaquopy
- Startup check and disable unsupported interfaces

diff --git a/electron/main.js b/electron/main.js
index 3b1b24f0..7404684e 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -42,10 +42,15 @@ const {
applyContentProtection,
applyContentProtectionToWindows,
} = require("./desktopPrivacySettings");
-const { installBrokenPipeGuards, safeConsoleLog } = require("./safeConsole");
+const { getLogsDir } = require("./backendCrashReport");
+const { installBrokenPipeGuards, createMainProcessLogger } = require("./safeConsole");
installBrokenPipeGuards(process);
+const mainProcessLogger = createMainProcessLogger({
+ getLogsDir: () => getLogsDir(getDefaultStorageDir()),
+});
+
// remember main window
var mainWindow = null;
var closeSettings = null;
@@ -87,7 +92,7 @@ try {
const disableGpuFile = path.join(storageDir, "disable-gpu");
if (fs.existsSync(disableGpuFile)) {
app.disableHardwareAcceleration();
- safeConsoleLog("Hardware acceleration disabled via storage flag.");
+ mainProcessLogger.write("Hardware acceleration disabled via storage flag.");
}
} catch {
// ignore errors reading storage dir this early
@@ -105,7 +110,7 @@ if (process.platform === "linux") {
// Detect if running in Flatpak sandbox
const isRunningInFlatpak = !!process.env.FLATPAK_ID;
if (isRunningInFlatpak) {
- safeConsoleLog(`Running in Flatpak sandbox: ${process.env.FLATPAK_ID}`);
+ mainProcessLogger.write(`Running in Flatpak sandbox: ${process.env.FLATPAK_ID}`);
}
// Protocol registration
@@ -662,8 +667,9 @@ function attachDefaultContextMenu(browserWindow) {
}
function log(message) {
- // log to stdout of this process (AppImage may close the pipe later)
- safeConsoleLog(message);
+ // Durable file under storage/logs/meshchatx.log. Stdout only when a TTY
+ // is attached (desktop AppImage launchers usually close the pipe).
+ mainProcessLogger.write(message);
// make sure main window exists
if (!mainWindow) {

diff --git a/electron/safeConsole.js b/electron/safeConsole.js
index 68c6512e..cb69abad 100644
--- a/electron/safeConsole.js
+++ b/electron/safeConsole.js
@@ -1,10 +1,19 @@
// SPDX-License-Identifier: 0BSD
/**
- * Broken-pipe (EPIPE) guards for Electron AppImage / launcher sessions where
- * stdout or stderr is closed while the main process still logs.
+ * Main-process log policy for Electron.
+ *
+ * Desktop launchers (AppImage, .desktop) often start MeshChatX with stdout
+ * attached to a pipe that later closes. Writing console.log then raises EPIPE.
+ * Prefer a durable file under the storage logs dir, and only mirror to stdout
+ * when it is still a TTY (or when forced via env).
*/
+const fs = require("node:fs");
+const path = require("node:path");
+
+const ELECTRON_LOG_PREFIX = "[electron] ";
+
function isBrokenPipeError(err) {
if (!err || typeof err !== "object") {
return false;
@@ -18,7 +27,7 @@ function isBrokenPipeError(err) {
/**
* Attach no-op error listeners so closed stdout/stderr do not become
- * uncaughtException dialogs when console.log writes after the pipe closes.
+ * uncaughtException dialogs when a write races the pipe close.
* @param {{ stdout?: NodeJS.WritableStream, stderr?: NodeJS.WritableStream } | null | undefined} [proc]
*/
function installBrokenPipeGuards(proc = process) {
@@ -40,6 +49,22 @@ function installBrokenPipeGuards(proc = process) {
attach(proc?.stderr);
}
+/**
+ * True when stdout is a live terminal, or MESHCHAT_FORCE_STDOUT_LOG=1.
+ * Packaged AppImage / desktop launches usually have isTTY false.
+ * @param {{ stdout?: { isTTY?: boolean } } | null | undefined} [proc]
+ * @param {NodeJS.ProcessEnv} [env]
+ */
+function shouldMirrorStdout(proc = process, env = process.env) {
+ if (env?.MESHCHAT_DISABLE_STDOUT_LOG === "1") {
+ return false;
+ }
+ if (env?.MESHCHAT_FORCE_STDOUT_LOG === "1") {
+ return true;
+ }
+ return Boolean(proc?.stdout && proc.stdout.isTTY);
+}
+
/**
* console.log that never throws on a closed stdout pipe.
* @param {...unknown} args
@@ -54,8 +79,110 @@ function safeConsoleLog(...args) {
}
}
+/**
+ * @param {string} message
+ * @returns {string}
+ */
+function formatElectronLogLine(message) {
+ const text = String(message ?? "").replace(/\s+$/u, "");
+ return `${new Date().toISOString()} ${ELECTRON_LOG_PREFIX}${text}\n`;
+}
+
+/**
+ * Create a durable main-process logger that appends to meshchatx.log.
+ * @param {{
+ * getLogsDir: () => string | null | undefined,
+ * appendFileSync?: typeof fs.appendFileSync,
+ * mkdirSync?: typeof fs.mkdirSync,
+ * now?: () => Date,
+ * }} options
+ */
+function createMainProcessLogger(options) {
+ const getLogsDir = options.getLogsDir;
+ const appendFileSync = options.appendFileSync || fs.appendFileSync.bind(fs);
+ const mkdirSync = options.mkdirSync || fs.mkdirSync.bind(fs);
+ const pending = [];
+ let disabled = false;
+
+ function resolveLogPath() {
+ let logsDir;
+ try {
+ logsDir = getLogsDir?.();
+ } catch {
+ return null;
+ }
+ if (!logsDir || typeof logsDir !== "string") {
+ return null;
+ }
+ return path.join(logsDir, "meshchatx.log");
+ }
+
+ function flushPending() {
+ if (!pending.length) {
+ return;
+ }
+ const logPath = resolveLogPath();
+ if (!logPath) {
+ return;
+ }
+ const chunk = pending.splice(0, pending.length).join("");
+ try {
+ mkdirSync(path.dirname(logPath), { recursive: true });
+ appendFileSync(logPath, chunk, { encoding: "utf8" });
+ } catch {
+ disabled = true;
+ }
+ }
+
+ function writeToFile(message) {
+ if (disabled) {
+ return;
+ }
+ const line = formatElectronLogLine(message);
+ const logPath = resolveLogPath();
+ if (!logPath) {
+ pending.push(line);
+ return;
+ }
+ flushPending();
+ try {
+ mkdirSync(path.dirname(logPath), { recursive: true });
+ appendFileSync(logPath, line, { encoding: "utf8" });
+ } catch {
+ disabled = true;
+ }
+ }
+
+ /**
+ * Durable file write plus best-effort stdout when a TTY is present.
+ * @param {unknown} message
+ * @param {{ mirrorStdout?: boolean, proc?: NodeJS.Process, env?: NodeJS.ProcessEnv }} [opts]
+ */
+ function write(message, opts = {}) {
+ writeToFile(message);
+ const mirror =
+ opts.mirrorStdout !== undefined
+ ? opts.mirrorStdout
+ : shouldMirrorStdout(opts.proc || process, opts.env || process.env);
+ if (mirror) {
+ safeConsoleLog(message);
+ }
+ }
+
+ return {
+ write,
+ flushPending,
+ _pendingForTests: pending,
+ _formatLineForTests: formatElectronLogLine,
+ };
+}
+
module.exports = {
isBrokenPipeError,
installBrokenPipeGuards,
safeConsoleLog,
+ shouldMirrorStdout,
+ formatElectronLogLine,
+ createMainProcessLogger,
+ ELECTRON_LOG_PREFIX,
};

diff --git a/meshchatx.rsm b/meshchatx.rsm
index dbefb9ed..c21ac352 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ

diff --git a/tests/electron/safeConsole.test.js b/tests/electron/safeConsole.test.js
index 34f3fd29..b2f3d398 100644
--- a/tests/electron/safeConsole.test.js
+++ b/tests/electron/safeConsole.test.js
@@ -8,6 +8,9 @@ const {
isBrokenPipeError,
installBrokenPipeGuards,
safeConsoleLog,
+ shouldMirrorStdout,
+ createMainProcessLogger,
+ ELECTRON_LOG_PREFIX,
} = require("../../electron/safeConsole");
describe("safeConsole", () => {
@@ -54,4 +57,79 @@ describe("safeConsole", () => {
});
expect(() => safeConsoleLog("x")).toThrow("boom");
});
+
+ it("shouldMirrorStdout is false without a TTY unless forced", () => {
+ expect(shouldMirrorStdout({ stdout: { isTTY: false } }, {})).toBe(false);
+ expect(shouldMirrorStdout({ stdout: { isTTY: true } }, {})).toBe(true);
+ expect(
+ shouldMirrorStdout({ stdout: { isTTY: false } }, { MESHCHAT_FORCE_STDOUT_LOG: "1" }),
+ ).toBe(true);
+ expect(
+ shouldMirrorStdout({ stdout: { isTTY: true } }, { MESHCHAT_DISABLE_STDOUT_LOG: "1" }),
+ ).toBe(false);
+ });
+
+ it("createMainProcessLogger appends durable lines and skips non-TTY stdout", () => {
+ const writes = [];
+ const mkdirCalls = [];
+ const logger = createMainProcessLogger({
+ getLogsDir: () => "/tmp/meshchat-logs",
+ mkdirSync: (dir, opts) => {
+ mkdirCalls.push([dir, opts]);
+ },
+ appendFileSync: (file, data) => {
+ writes.push([file, data]);
+ },
+ });
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ logger.write("backend ready", {
+ proc: { stdout: { isTTY: false } },
+ env: {},
+ });
+
+ expect(consoleSpy).not.toHaveBeenCalled();
+ expect(mkdirCalls.length).toBeGreaterThan(0);
+ expect(writes).toHaveLength(1);
+ expect(writes[0][0]).toBe("/tmp/meshchat-logs/meshchatx.log");
+ expect(writes[0][1]).toContain(ELECTRON_LOG_PREFIX);
+ expect(writes[0][1]).toContain("backend ready");
+ });
+
+ it("createMainProcessLogger mirrors to stdout when TTY is present", () => {
+ const logger = createMainProcessLogger({
+ getLogsDir: () => "/tmp/meshchat-logs",
+ mkdirSync: () => {},
+ appendFileSync: () => {},
+ });
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+ logger.write("tty line", {
+ proc: { stdout: { isTTY: true } },
+ env: {},
+ });
+ expect(consoleSpy).toHaveBeenCalledWith("tty line");
+ });
+
+ it("createMainProcessLogger buffers until logs dir is available", () => {
+ let logsDir = null;
+ const writes = [];
+ const logger = createMainProcessLogger({
+ getLogsDir: () => logsDir,
+ mkdirSync: () => {},
+ appendFileSync: (file, data) => {
+ writes.push([file, data]);
+ },
+ });
+ logger.write("early", { mirrorStdout: false });
+ expect(writes).toHaveLength(0);
+ expect(logger._pendingForTests.length).toBe(1);
+
+ logsDir = "/tmp/meshchat-logs";
+ logger.write("later", { mirrorStdout: false });
+ expect(writes.length).toBeGreaterThanOrEqual(1);
+ const joined = writes.map((entry) => entry[1]).join("");
+ expect(joined).toContain("early");
+ expect(joined).toContain("later");
+ expect(logger._pendingForTests.length).toBe(0);
+ });
});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────